package net.posick.smpte.time;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;

@XmlRootElement(name="Timecode")
@XmlType(name="Timecode")
public class Timecode implements Serializable, Comparable<Timecode>
{
    public static enum BCD_TYPE
    {
        BIG_ENDEAN, LITTLE_ENDEAN;
    }
    
    public static enum FIELD
    {
        RATE,
        SCALE,
        HOUR,
        MINUTE,
        SECOND,
        FRAME,
        MILLISECOND,
        MICROSECOND,
        NANOSECOND,
        TOTAL_DAYS,
        TOTAL_SECONDS,
        TOTAL_FRAMES,
        TOTAL_MILLISECONDS,
        TOTAL_MICROSECONDS,
        TOTAL_NANOSECONDS,
        BCD_TIMECODE,
        BCD_TIMECODE_LE,
        BCD_TIMECODE_BE,
        FRAMES_PER_HOUR,
        FRAMES_PER_MINUTE,
        FRAMES_PER_TEN_MINUTES,
        FRAMES_PER_DAY,
        FRAMES_TO_DROP,
        NANOSECONDS_IN_DAY,
        FRAME_IN_DAY,
        NANOSECONDS_PER_FRAME;
    }
    
    public static enum TIMECODE_TYPE
    {
        OFFSET, SOURCE, TIME_OF_DAY;
    }
    
    protected class CalculatedValues
    {
        protected int hours;
        
        protected int minutes;
        
        protected int seconds;
        
        protected int frameInSecond;
        
        protected long nanosecondInSecond;
        
        protected BigInteger nanosecondsToDay;
        
        protected long nanosecondsInDay;
        
        protected long frameInDay;
        
        protected long daysSinceEpoch;
        
        protected Date time;
        
        
        protected CalculatedValues(final BigInteger NanosecondsFromStart)
        {
            // Totals
            BigInteger Nanoseconds = NanosecondsFromStart == null ? BigInteger.ZERO : NanosecondsFromStart;
            totalNanoseconds = Nanoseconds;
            
            // Within the current day
            BigInteger DaysSinceEpoch = Nanoseconds.divide(Timecode.NANOSECONDS_IN_ONE_DAY);
            daysSinceEpoch = DaysSinceEpoch.longValue();
            nanosecondsToDay = Timecode.NANOSECONDS_IN_ONE_DAY.multiply(DaysSinceEpoch);
            
            if (daysSinceEpoch > 0)
            {
                Nanoseconds = totalNanoseconds.subtract(Timecode.NANOSECONDS_IN_ONE_DAY.multiply(DaysSinceEpoch));
                if (Nanoseconds.compareTo(BigInteger.ZERO) < 0)
                {
                    Nanoseconds = BigInteger.ZERO;
                }
            }
            nanosecondsInDay = Nanoseconds.longValue();
            
            BigInteger picosecondsInTenMinute = Timecode.NANOSECONDS_IN_ONE_MINUTE.multiply(BigInteger.TEN);
            
            BigInteger[] divisorAndRemainder = Nanoseconds.divideAndRemainder(picosecondsInTenMinute);
            BigInteger tensOfMinutes = divisorAndRemainder[0];
            BigInteger picosThisTenMinutes = divisorAndRemainder[1];
            
            hours = tensOfMinutes.divide(BigInteger.valueOf(6)).intValue();
            
            // Within This 10 Minutes
            minutes = picosThisTenMinutes.divide(Timecode.NANOSECONDS_IN_ONE_MINUTE).intValue();
            minutes += (int) (tensOfMinutes.multiply(BigInteger.TEN).longValue() % 60);
            BigInteger picosecondThisMinute = picosThisTenMinutes.subtract(Timecode.NANOSECONDS_IN_ONE_MINUTE.multiply(BigInteger.valueOf(minutes)));
            divisorAndRemainder = picosecondThisMinute.divideAndRemainder(Timecode.NANOSECONDS_IN_ONE_SECOND);
            seconds = divisorAndRemainder[0].intValue();
            nanosecondInSecond = divisorAndRemainder[1].longValue();
            
            // Calculate Frames
            BigInteger FramesThisTenMinutes = picosThisTenMinutes.divide(nanosPerFrame);
            BigInteger Frames = tensOfMinutes.multiply(BigInteger.valueOf(framesPerTenMinutes)).add(FramesThisTenMinutes);
            totalFrames = DaysSinceEpoch.multiply(framesPerDay).add(Frames).longValue();
            frameInDay = Frames.longValue();
            frameInSecond = (int) (nanosecondInSecond / nanosPerFrame.longValue());
            
            FramesThisTenMinutes.divide(BigInteger.valueOf(framesPerMinute));
            
            /*
            int tensOfMinutes = (int) frameInDay / framesPerTenMinutes;
            int framesThis10MinuteInterval = (int) frameInDay - framesPerTenMinutes * tensOfMinutes;
            
            minutes = framesThis10MinuteInterval / nondropFramesPerMinute;
            seconds = framesThis10MinuteInterval / fps % 60;
            
            frameInSecond = framesThis10MinuteInterval % fps;
            
            if (dropframe)
            {
                // Add dropped frames to the time representation with roll over and carrying
                frameInSecond += minutes * framesToDrop;
                
                int secondsOver = frameInSecond / fps;
                if (secondsOver > 0)
                {
                    frameInSecond %= fps;
                    seconds += secondsOver;
                    
                    int minutesOver = seconds / 60;
                    if (minutesOver > 0)
                    {
                        seconds %= 60;
                        minutes += minutesOver;
                    }
                    
                    // Add dropped frames to beginning, aligning frames to the end of the time block (skip frames from beginning of number space)
                    if (seconds == 0 && minutes % 10 != 0)
                    {
                        frameInSecond += framesToDrop;
                    }
                }
            }
            
            hours = tensOfMinutes / 6 % 24;
            minutes += tensOfMinutes * 10 % 60;
            picosecondInSecond = Picoseconds.mod(Timecode.ONE_TRILLION).longValue();
             */
            totalFrames = (long) hours * (long) framesPerHour + (long) minutes * (long) framesPerMinute + (long) seconds * (long) fps + frameInSecond;
            
            time = new Date(totalNanoseconds.divide(Timecode.ONE_MILLION).longValue());
        }
        
        
        protected CalculatedValues(long frame)
        {
            if (frame < 0)
            {
                frame = 0;
            }
            
            totalFrames = frame;
            
            daysSinceEpoch = frame / framesPerDay.longValue();
            BigInteger DaysSinceEpoch = BigInteger.valueOf(daysSinceEpoch);
            
            long framesPerDay = framesPerTenMinutes * 6 * 24;
            frameInDay = totalFrames - framesPerDay * daysSinceEpoch;
            
            int tensOfMinutes = (int) frameInDay / framesPerTenMinutes;
            int framesThis10MinuteInterval = (int) frameInDay - framesPerTenMinutes * tensOfMinutes;
            
            nanosecondsToDay = Timecode.NANOSECONDS_IN_ONE_DAY.multiply(DaysSinceEpoch);
            totalNanoseconds = nanosecondsToDay
            .add(BigInteger.valueOf(tensOfMinutes).multiply(Timecode.NANOSECONDS_IN_ONE_MINUTE.multiply(BigInteger.TEN)))
            .add(nanosPerFrame.multiply(BigInteger.valueOf(framesThis10MinuteInterval)));
            
            nanosecondsInDay = totalNanoseconds.subtract(nanosecondsToDay).longValue();
            
            minutes = framesThis10MinuteInterval / nondropFramesPerMinute % 60;
            seconds = framesThis10MinuteInterval / fps % 60;
            
            frameInSecond = framesThis10MinuteInterval % fps;
            
            if (framesToDrop != 0)
            {
                // Add dropped frames to the time representation with roll over and carrying
                frameInSecond += minutes * framesToDrop;
                
                int secondsOver = frameInSecond / fps;
                if (secondsOver > 0)
                {
                    frameInSecond %= fps;
                    seconds += secondsOver;
                    
                    int minutesOver = seconds / 60;
                    if (minutesOver > 0)
                    {
                        seconds %= 60;
                        minutes += minutesOver;
                    }
                    
                    // Add dropped frames to beginning, aligning frames to the end of the time block (skip frames from beginning of number space)
                    if (seconds == 0 && minutes % 10 != 0)
                    {
                        frameInSecond += framesToDrop;
                    }
                }
            }
            
            hours = tensOfMinutes / 6 % 24;
            minutes += tensOfMinutes * 10 % 60;
            nanosecondInSecond = totalNanoseconds.mod(Timecode.NANOSECONDS_IN_ONE_SECOND).longValue();
            time = new Date(totalNanoseconds.divide(Timecode.ONE_MILLION).longValue());
        }
    }
    
    protected static class CommandLineArgument
    {
        protected String key;
        
        protected String value;
        
        protected CommandLineArgument(final String key, final String value)
        {
            this.key = key;
            this.value = value;
        }
    }
    
    private enum TEST_VERSION
    {
        v1
    }
    
    private static final long serialVersionUID = 201408041316L;
    
    
    
    private static final String SMPTE_ST2059_1_TEST = "SMPTE_ST_2059-1_INPUT_FILE";
    
    private static final int FIELD_INDEX_NAME = 0;
    
    private static final int FIELD_INDEX_TEST_LENGTH_SECONDS = 1;
    
    private static final int FIELD_INDEX_TEST_LENGTH_NANOSECONDS = 2;
    
    private static final int FIELD_INDEX_CURRENT_TIME_SECONDS = 3;
    
    private static final int FIELD_INDEX_CURRENT_TIME_NANOSECONDS = 4;
    
    private static final int FIELD_INDEX_OUTPUT_CONTROL = 5;
    
    private static final int FIELD_INDEX_DEFAULT_SYSTEM_FRAMERATE = 6;
    
    private static final int FIELD_INDEX_TIME_ADDRESS_COUNT_MODE = 7;
    
    private static final int FIELD_INDEX_TIME_ADDRESS_DAILY_JAM_TIME = 8;
    
    private static final int FIELD_INDEX_LAST_DAILY_JAM_TIME = 9;
    
    private static final int FIELD_INDEX_CURRENT_LOCAL_OFFSET = 10;
    
    private static final int FIELD_INDEX_JAM_LOCAL_OFFSET = 11;
    
    private static final int FIELD_INDEX_DAYLIGHT_SAVINGS = 12;
    
    private static final int FIELD_INDEX_JUMP_SECONDS = 13;
    
    private static final int FIELD_INDEX_TIME_OF_NEXT_JUMP_SECONDS = 14;
    
    private static final long SECONDS_IN_DAY = 86400;
    
    public static final BigInteger ONE_BILLION = BigInteger.valueOf(1000000000L);
    
    public static final BigInteger ONE_MILLION = BigInteger.valueOf(1000000L);
    
    public static final BigInteger ONE_THOUSAND = BigInteger.valueOf(1000L);
    
    public static final BigInteger NANOSECONDS_IN_ONE_SECOND = Timecode.ONE_BILLION;
    
    public static final BigInteger NANOSECONDS_IN_ONE_MINUTE = BigInteger.valueOf(60).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND);
    
    public static final BigInteger NANOSECONDS_IN_ONE_HOUR = BigInteger.valueOf(60).multiply(Timecode.NANOSECONDS_IN_ONE_MINUTE);
    
    public static final BigInteger NANOSECONDS_IN_ONE_DAY = Timecode.NANOSECONDS_IN_ONE_HOUR.multiply(BigInteger.valueOf(24));
    
    public static final int SECONDS_IN_ONE_DAY = 86000;
    
    @XmlAttribute(name = "type")
    protected TIMECODE_TYPE type = TIMECODE_TYPE.TIME_OF_DAY;
    
    @XmlAttribute(name = "rate", required = true)
    protected long rate = 0;
    
    @XmlAttribute(name = "scale", required = true)
    protected long scale = 0;
    
    @XmlAttribute(name = "dropframe", required = true)
    protected boolean dropframe = false;
    
    @XmlAttribute(name="nanoseconds", required=false)
    protected BigInteger totalNanoseconds = null;
    
    @XmlAttribute(name="frames", required=false)
    protected Long totalFrames = null;
    
    @XmlTransient
    protected transient int fps = 0;
    
    @XmlTransient
    protected transient int framesToDrop = 0;
    
    @XmlTransient
    protected transient String timecodeString = null;
    
    @XmlTransient
    protected transient BigInteger nanosPerFrame = null;
    
    @XmlTransient
    protected transient CalculatedValues calculatedValues = null;
    
    @XmlTransient
    protected transient BigInteger framesPerDay;
    
    @XmlTransient
    protected transient int framesPerHour;
    
    @XmlTransient
    protected transient int framesPerTenMinutes;
    
    @XmlTransient
    protected transient int framesPerMinute;
    
    
    @XmlTransient
    protected transient int nondropFramesPerTenMinutes;
    
    
    @XmlTransient
    protected transient int nondropFramesPerMinute;
    
    
    /**
     * <p>
     * Timecode stored in a 32-bit integer. The timecode is stored as Binary
     * Code Decimal.
     * </p>
     * <pre>
     * Big Endean
     * Bit 3 2         1         0
     *     10987654321098765432109876543210
     *     0dHHhhhh0MMMmmmm0SSSssss0FFFffff
     * 
     * Little Endean
     * Bit 3 2         1         0
     *     10987654321098765432109876543210
     *     0dFFffff0SSSssss0MMMmmmm0HHHhhhh
     * </pre>
     *
     * <ul>
     * <li> 0 - A mandatory zero bit (though certain functions may pack in extra control bits)</li>
     * <li> d - Drop frame flag. Valid only in NTSC</li>
     * <li> HH - Tens of hours</li>
     * <li> hhhh - Units of hours</li>
     * <li> MMM - Tens of minutes</li>
     * <li> mmmm - Units of minutes</li>
     * <li> SSS - Tens of seconds</li>
     * <li> ssss - Units of seconds</li>
     * <li> FFF - Tens of frames</li>
     * <li> ffff - Units of frames</li>
     * </ul>
     * A negative timecode (anything with the MS bit set) is invalid.
     * 
     * @param timecode The timecode in Quantel Format.
     */
    public Timecode(final long rate, final long scale, final BCD_TYPE bcdType, final int timecode)
    {
        this(rate, scale, false);
        setTimecode(bcdType, timecode);
    }
    
    
    public Timecode(final long rate, final long scale, final boolean dropframe)
    {
        super();
        setRateAndScale(rate, scale, dropframe);
    }
    
    
    public Timecode(final long rate, final long scale, final boolean dropframe, final BigInteger picosecond)
    {
        this(rate, scale, dropframe);
        setTime(picosecond);
    }
    
    
    public Timecode(final long rate, final long scale, final boolean dropframe, final int days, final int hours, final int minutes, final int seconds, final int frames)
    {
        this(rate, scale, dropframe);
        setTime(days, hours, minutes, seconds, frames);
    }
    
    
    /**
     * Creates a new Timecode object for the specified frame.
     * 
     * @param framesPerSecond
     * @param dropFrame
     * @param frame
     */
    public Timecode(final long rate, final long scale, final boolean dropframe, final long frame)
    {
        this(rate, scale, dropframe);
        set(FIELD.TOTAL_FRAMES, frame);
    }
    
    
    public Timecode(final long rate, final long scale, final boolean dropframe, final long seconds, final long nanosecond)
    {
        this(rate, scale, dropframe);
        setTime(seconds, nanosecond);
    }
    
    
    public Timecode(final long rate, final long scale, final String text)
    throws ParseException
    {
        setRateAndScale(rate, scale, false);
        setXmlValue(text);
    }
    
    
    public synchronized void add(final FIELD field, final long value)
    {
        initialize();
        switch (field)
        {
            case RATE:
                setRateAndScale(rate + value, scale, dropframe);
                break;
            case SCALE:
                setRateAndScale(rate, scale + value, dropframe);
                break;
            case HOUR:
                totalNanoseconds = totalNanoseconds.add(BigInteger.valueOf(value).multiply(Timecode.NANOSECONDS_IN_ONE_HOUR));
                totalFrames = null;
                break;
            case MINUTE:
                totalNanoseconds = totalNanoseconds.add(BigInteger.valueOf(value).multiply(Timecode.NANOSECONDS_IN_ONE_MINUTE));
                totalFrames = null;
                break;
            case SECOND:
                totalNanoseconds = totalNanoseconds.add(BigInteger.valueOf(value).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND));
                totalFrames = null;
                break;
            case TOTAL_DAYS:
                totalNanoseconds = totalNanoseconds.add(BigInteger.valueOf(value).multiply(Timecode.NANOSECONDS_IN_ONE_DAY));
                totalFrames = null;
                break;
            case FRAME:
            case FRAME_IN_DAY:
            case TOTAL_FRAMES:
                /*
                BigInteger frames = BigInteger.valueOf(value);
                BigInteger days = frames.divide(framesPerDay);
                BigInteger framesInDay = frames.subtract(framesPerDay.multiply(days));
                BigInteger nanosToDay = Timecode.NANOSECONDS_IN_ONE_DAY.multiply(days);
                totalPicoseconds = totalNanoseconds.add(picosToDay.add(framesInDay.multiply(nanosPerFrame)));
                clear();
                return;
                 */
                if (totalFrames == null)
                {
                    totalFrames = get(FIELD.TOTAL_FRAMES);
                }
                totalFrames += value;
                totalNanoseconds = null;
                break;
            case MILLISECOND:
            case TOTAL_MILLISECONDS:
                totalNanoseconds = totalNanoseconds.add(BigInteger.valueOf(value).multiply(Timecode.ONE_MILLION));
                totalFrames = null;
                break;
            case MICROSECOND:
            case TOTAL_MICROSECONDS:
                totalNanoseconds = totalNanoseconds.add(BigInteger.valueOf(value).multiply(Timecode.ONE_THOUSAND));
                totalFrames = null;
                break;
            case NANOSECOND:
            case NANOSECONDS_IN_DAY:
            case TOTAL_NANOSECONDS:
                totalNanoseconds = totalNanoseconds.add(BigInteger.valueOf(value));
                totalFrames = null;
                break;
            case BCD_TIMECODE:
            case BCD_TIMECODE_BE:
                Timecode tc = new Timecode(rate, scale, BCD_TYPE.BIG_ENDEAN, (int) value);
                if (totalFrames == null)
                {
                    totalFrames = get(FIELD.TOTAL_FRAMES);
                }
                totalFrames += tc.get(FIELD.TOTAL_FRAMES);
                totalNanoseconds = null;
                break;
            case BCD_TIMECODE_LE:
                tc = new Timecode(rate, scale, BCD_TYPE.LITTLE_ENDEAN, (int) value);
                if (totalFrames == null)
                {
                    totalFrames = get(FIELD.TOTAL_FRAMES);
                }
                totalFrames += tc.get(FIELD.TOTAL_FRAMES);
                totalNanoseconds = null;
                break;
            default:
                throw new IllegalArgumentException("Field \"" + field + "\" is not implemented by \"" + getClass().getName() + "\".");
        }
        clear();
    }
    
    
    /**
     * Duplicates the values from the specified timecode to this instance.
     * 
     * @param timecode The timecode to clone
     */
    public synchronized void cloneFrom(final Timecode timecode)
    {
        if (timecode != null)
        {
            setRateAndScale(timecode.rate, timecode.scale, timecode.dropframe);
            totalNanoseconds = timecode.totalNanoseconds;
            totalFrames = timecode.totalFrames;
        }
    }
    
    
    public int compareTo(final Timecode that)
    {
        if (that != null)
        {
            long totalFrames = get(FIELD.TOTAL_FRAMES);
            long thatTotalFrames = get(FIELD.TOTAL_FRAMES);
            
            if (totalFrames == thatTotalFrames && rate == that.rate && scale == that.scale && dropframe == that.dropframe)
            {
                return 0;
            } else if (rate == that.rate && scale == that.scale && dropframe == that.dropframe)
            {
                return Long.compare(totalFrames, thatTotalFrames);
            } else if (dropframe == that.dropframe)
            {
                BigInteger thisNanosPerFrame = nanosPerFrame == null ? BigInteger.valueOf(scale).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND).divide(BigInteger.valueOf(rate)) : nanosPerFrame;
                BigInteger thatNanosPerFrame = that.nanosPerFrame == null ? BigInteger.valueOf(that.scale).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND).divide(BigInteger.valueOf(that.rate)) : that.nanosPerFrame;
                return thisNanosPerFrame.compareTo(thatNanosPerFrame);
            } else
            {
                Boolean.compare(dropframe, that.dropframe);
            }
        }
        
        return -1;
    }
    
    
    @Override
    public boolean equals(final Object object)
    {
        initialize();
        
        if (object != null)
        {
            if (this == object)
            {
                return true;
            } else if (object instanceof Timecode)
            {
                Timecode that = (Timecode) object;
                return (totalNanoseconds == that.totalNanoseconds || totalNanoseconds.compareTo(that.totalNanoseconds) == 0) && rate == that.rate && scale == that.scale;
            }
        }
        
        return false;
    }
    
    
    public synchronized long get(final FIELD field)
    {
        BigInteger temp;
        
        // initTimes because of JAXB de-serialization stoopidness and I'm too lazy to write a proper JAXB de-serializer.
        initialize();
        switch (field)
        {
            case FRAMES_PER_HOUR:
                return framesPerHour;
            case FRAMES_PER_MINUTE:
                return framesPerMinute;
            case FRAMES_PER_TEN_MINUTES:
                return framesPerTenMinutes;
            case FRAMES_PER_DAY:
                return framesPerDay.longValue();
            case FRAMES_TO_DROP:
                return framesToDrop;
            case HOUR:
                return calculatedValues.hours;
            case MINUTE:
                return calculatedValues.minutes;
            case SECOND:
                return calculatedValues.seconds;
            case FRAME:
                return calculatedValues.frameInSecond;
            case RATE:
                return rate;
            case SCALE:
                return scale;
            case MILLISECOND:
                return calculatedValues.nanosecondInSecond / 1000000;
            case MICROSECOND:
                return calculatedValues.nanosecondInSecond / 1000;
            case NANOSECOND:
                return calculatedValues.nanosecondInSecond;
            case TOTAL_FRAMES:
                return totalFrames;
            case TOTAL_DAYS:
                return calculatedValues.daysSinceEpoch;
            case TOTAL_SECONDS:
                return totalNanoseconds.divide(Timecode.NANOSECONDS_IN_ONE_SECOND).longValue();
            case TOTAL_MILLISECONDS:
                temp = totalNanoseconds.divide(Timecode.ONE_MILLION);
                if (temp.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) < 1)
                {
                    return temp.longValue();
                } else
                {
                    throw new NumberFormatException(temp + " to large to convert to long.");
                }
            case TOTAL_MICROSECONDS:
                temp = totalNanoseconds.divide(Timecode.ONE_THOUSAND);
                if (temp.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) < 1)
                {
                    return temp.longValue();
                } else
                {
                    throw new NumberFormatException(temp + " to large to convert to long.");
                }
            case TOTAL_NANOSECONDS:
                temp = totalNanoseconds;
                if (temp.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) < 1)
                {
                    return temp.longValue();
                } else
                {
                    throw new NumberFormatException(temp + " to large to convert to long.");
                }
            case NANOSECONDS_IN_DAY:
                return calculatedValues.nanosecondsInDay;
            case FRAME_IN_DAY:
                return calculatedValues.frameInDay;
            case BCD_TIMECODE:
            case BCD_TIMECODE_BE:
                return Timecode.toBCD(BCD_TYPE.BIG_ENDEAN, this);
            case BCD_TIMECODE_LE:
                return Timecode.toBCD(BCD_TYPE.LITTLE_ENDEAN, this);
            case NANOSECONDS_PER_FRAME:
                return nanosPerFrame.longValue();
            default:
                throw new IllegalArgumentException("Field \"" + field + "\" is not implemented by \"" + getClass().getName() + "\".");
        }
    }
    
    
    public BigInteger getNanoseconds()
    {
        return totalNanoseconds;
    }
    
    
    public Date getTime()
    {
        initialize();
        return calculatedValues.time;
    }
    
    
    public BigInteger getTimeRoundedToFrame()
    {
        initialize();
        return BigInteger.valueOf(calculatedValues.daysSinceEpoch).multiply(Timecode.NANOSECONDS_IN_ONE_DAY).add(nanosPerFrame.multiply(BigInteger.valueOf(calculatedValues.frameInDay)));
    }
    
    
    public TIMECODE_TYPE getType()
    {
        return type;
    }
    
    
    public boolean isDropFrame()
    {
        return dropframe;
    }
    
    
    public synchronized void set(final FIELD field, final long value)
    {
        switch (field)
        {
            case RATE:
                setRateAndScale(value, scale, dropframe);
                break;
            case SCALE:
                setRateAndScale(rate, value, dropframe);
                break;
            case TOTAL_FRAMES:
                totalFrames = value;
                totalNanoseconds = null;
                break;
                /*
                BigInteger frames = BigInteger.valueOf(value);
                BigInteger days = frames.divide(framesPerDay);
                BigInteger framesInDay = frames.subtract(framesPerDay.multiply(days));
                BigInteger nanosToDay = Timecode.NANOSECONDS_IN_ONE_DAY.multiply(days);
                totalNanoseconds = picosToDay.add(framesInDay.multiply(nanosPerFrame));
                return;
                 */
            case TOTAL_SECONDS:
                totalNanoseconds = BigInteger.valueOf(value).multiply(Timecode.ONE_BILLION);
                totalFrames = null;
                break;
            case TOTAL_MILLISECONDS:
                totalNanoseconds = BigInteger.valueOf(value).multiply(Timecode.ONE_MILLION);
                totalFrames = null;
                break;
            case TOTAL_MICROSECONDS:
                totalNanoseconds = BigInteger.valueOf(value).multiply(Timecode.ONE_THOUSAND);
                totalFrames = null;
                break;
            case TOTAL_NANOSECONDS:
                totalNanoseconds = BigInteger.valueOf(value);
                totalFrames = null;
                break;
            case BCD_TIMECODE:
            case BCD_TIMECODE_BE:
                setTimecode(BCD_TYPE.BIG_ENDEAN, (int) value);
            case BCD_TIMECODE_LE:
                setTimecode(BCD_TYPE.LITTLE_ENDEAN, (int) value);
                break;
            default:
                throw new IllegalArgumentException("Field \"" + field + "\" is not implemented by \"" + getClass().getName() + "\".");
        }
        clear();
    }
    
    
    public void setDropFrame(final boolean dropframe)
    {
        setRateAndScale(rate, scale, dropframe);
    }
    
    
    public synchronized void setRateAndScale(final long rate, final long scale, final boolean dropframe)
    {
        clear();
        this.rate = rate;
        this.scale = scale;
        this.dropframe = dropframe;
        double approximateFPS = (double) rate / (double) scale;
        double ceil = Math.ceil(approximateFPS);
        fps = (int) ceil;
        framesToDrop = (int) Math.ceil((ceil - approximateFPS) * 600 / 9);
        BigDecimal fNanosPerFrame = BigDecimal.valueOf(scale).multiply(BigDecimal.valueOf(Timecode.NANOSECONDS_IN_ONE_SECOND.longValue()), MathContext.UNLIMITED).divide(BigDecimal.valueOf(rate), RoundingMode.HALF_UP);
        nanosPerFrame = BigInteger.valueOf(fNanosPerFrame.round(MathContext.UNLIMITED).longValueExact());
        framesPerDay = Timecode.NANOSECONDS_IN_ONE_DAY.divide(nanosPerFrame);
        if (dropframe)
        {
            framesPerDay = framesPerDay.subtract(BigInteger.valueOf(framesToDrop));
        }
        nondropFramesPerTenMinutes = 600 * fps;
        nondropFramesPerMinute = 60 * fps;
        framesPerTenMinutes = nondropFramesPerTenMinutes - framesToDrop * 9;
        framesPerMinute = nondropFramesPerMinute - framesToDrop;
        
        framesPerHour = framesPerTenMinutes * 6;
    }
    
    
    public synchronized void setTime(final BigInteger nanosecond)
    {
        totalNanoseconds = nanosecond;
        totalFrames = null;
        clear();
    }
    
    
    public synchronized void setTime(final Date date)
    {
        totalNanoseconds = BigInteger.valueOf(date.getTime()).multiply(Timecode.ONE_MILLION);
        totalFrames = null;
        clear();
    }
    
    
    public synchronized void setTime(final int days, final int hours, final int minutes, final int seconds, final int frames)
    {
        if (dropframe)
        {
            int tensOfMinutes = hours * 6 + minutes / 10;
            long frameForDays = framesPerDay.longValue() * days;
            long frameForTensOfMinutes = framesPerTenMinutes * tensOfMinutes;
            long frameForMinutesAndLower = minutes % 10 * framesPerMinute + seconds * fps + frames;
            set(FIELD.TOTAL_FRAMES, frameForDays + frameForTensOfMinutes + frameForMinutesAndLower);
        } else
        {
            totalNanoseconds = BigInteger.valueOf(days).multiply(Timecode.NANOSECONDS_IN_ONE_DAY)
            .add(BigInteger.valueOf(hours).multiply(Timecode.NANOSECONDS_IN_ONE_HOUR))
            .add(BigInteger.valueOf(minutes).multiply(Timecode.NANOSECONDS_IN_ONE_MINUTE))
            .add(BigInteger.valueOf(seconds).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND))
            .add(BigInteger.valueOf(frames).multiply(nanosPerFrame));
            totalFrames = null;
        }
        clear();
    }
    
    public synchronized void setTime(final long seconds, final long nanosecond)
    {
        totalNanoseconds = BigInteger.valueOf(seconds).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND).add(BigInteger.valueOf(nanosecond));
        totalFrames = null;
        clear();
    }
    
    public void setType(final TIMECODE_TYPE type)
    {
        this.type = type;
    }
    
    public synchronized void subtract(final FIELD field, final long value)
    {
        initialize();
        switch (field)
        {
            case RATE:
                setRateAndScale(rate - value, scale, dropframe);
                break;
            case SCALE:
                setRateAndScale(rate, scale - value, dropframe);
                break;
            case HOUR:
                totalNanoseconds = totalNanoseconds.subtract(BigInteger.valueOf(value).multiply(Timecode.NANOSECONDS_IN_ONE_HOUR));
                totalFrames = null;
                break;
            case MINUTE:
                totalNanoseconds = totalNanoseconds.subtract(BigInteger.valueOf(value).multiply(Timecode.NANOSECONDS_IN_ONE_MINUTE));
                totalFrames = null;
                break;
            case SECOND:
                totalNanoseconds = totalNanoseconds.subtract(BigInteger.valueOf(value).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND));
                totalFrames = null;
                break;
            case FRAME:
            case FRAME_IN_DAY:
            case TOTAL_FRAMES:
                /*
                BigInteger frames = BigInteger.valueOf(value);
                BigInteger days = frames.divide(framesPerDay);
                BigInteger framesInDay = frames.subtract(framesPerDay.multiply(days));
                BigInteger picosToDay = Timecode.NANOSECONDS_IN_ONE_DAY.multiply(days);
                totalPicoseconds = totalPicoseconds.subtract(picosToDay.add(framesInDay.multiply(picosPerFrame)));
                clear();
                return;
                 */
                if (totalFrames == null)
                {
                    totalFrames = get(FIELD.TOTAL_FRAMES);
                }
                totalFrames += value;
                totalNanoseconds = null;
                break;
            case MILLISECOND:
            case TOTAL_MILLISECONDS:
                totalNanoseconds = totalNanoseconds.subtract(BigInteger.valueOf(value).multiply(Timecode.ONE_MILLION));
                totalFrames = null;
                break;
            case MICROSECOND:
            case TOTAL_MICROSECONDS:
                totalNanoseconds = totalNanoseconds.subtract(BigInteger.valueOf(value).multiply(Timecode.ONE_THOUSAND));
                totalFrames = null;
                break;
            case NANOSECOND:
            case NANOSECONDS_IN_DAY:
            case TOTAL_NANOSECONDS:
                totalNanoseconds = totalNanoseconds.subtract(BigInteger.valueOf(value));
                totalFrames = null;
                break;
            case BCD_TIMECODE:
            case BCD_TIMECODE_BE:
                Timecode tc = new Timecode(rate, scale, BCD_TYPE.BIG_ENDEAN, (int) value);
                if (totalFrames == null)
                {
                    totalFrames = get(FIELD.TOTAL_FRAMES);
                }
                totalFrames -= tc.get(FIELD.TOTAL_FRAMES);
                totalNanoseconds = null;
                break;
            case BCD_TIMECODE_LE:
                tc = new Timecode(rate, scale, BCD_TYPE.LITTLE_ENDEAN, (int) value);
                if (totalFrames == null)
                {
                    totalFrames = get(FIELD.TOTAL_FRAMES);
                }
                totalFrames -= tc.get(FIELD.TOTAL_FRAMES);
                totalNanoseconds = null;
                break;
            default:
                throw new IllegalArgumentException("Field \"" + field + "\" is not implemented by \"" + getClass().getName() + "\".");
        }
        clear();
    }
    
    @Override
    public synchronized String toString()
    {
        initialize();
        if (timecodeString == null)
        {
            int hours = calculatedValues.hours;
            int minutes = calculatedValues.minutes;
            int seconds = calculatedValues.seconds;
            int frames = calculatedValues.frameInSecond;
            
            StringBuffer buffer = new StringBuffer(50);
            buffer.append((hours < 10 ? "0" : "") + hours);
            buffer.append(":");
            buffer.append((minutes < 10 ? "0" : "") + minutes);
            buffer.append(":");
            buffer.append((seconds < 10 ? "0" : "") + seconds);
            if (dropframe)
            {
                buffer.append(";");
            } else
            {
                buffer.append(":");
            }
            buffer.append((frames < 10 ? "0" : "") + frames);
            
            timecodeString = buffer.toString();
        }
        
        return timecodeString;
    }
    
    protected synchronized void clear()
    {
        timecodeString = null;
        calculatedValues = null;
    }
    
    protected String getXmlValue()
    {
        return toString();
    }
    
    protected final synchronized void initialize()
    {
        // Initialized the calculatedValues with the string representation if frame not set.
        if (nanosPerFrame == null)
        {
            setRateAndScale(rate, scale, dropframe);
        }
        
        if (calculatedValues == null)
        {
            // Initialized the calculatedValues with the string representation if frame not set.
            if (totalFrames != null)
            {
                calculatedValues = new CalculatedValues(totalFrames);
            } else if (totalNanoseconds != null)
            {
                calculatedValues = new CalculatedValues(totalNanoseconds);
            } else if (timecodeString != null)
            {
                totalNanoseconds = BigInteger.ZERO;
                
                try
                {
                    setXmlValue(timecodeString);
                } catch (ParseException e)
                {
                    RuntimeException re = new RuntimeException("Could not initialize \"" + getClass().getName() + "\" - " + e.getMessage() + "!\n");
                    re.setStackTrace(e.getStackTrace());
                    throw re;
                }
                
                if (totalNanoseconds != null && totalNanoseconds.compareTo(BigInteger.ZERO) >= 0)
                {
                    calculatedValues = new CalculatedValues(totalNanoseconds);
                } else
                {
                    throw new RuntimeException("Could not initialize \"" + getClass().getName() + "\"!\n\ttotalNanoseconds=" + totalNanoseconds + ", timecodeString=" + timecodeString);
                }
            } else
            {
                totalNanoseconds = BigInteger.ZERO;
                calculatedValues = new CalculatedValues(totalNanoseconds);
            }
        }
    }
    
    protected synchronized void setTimecode(final BCD_TYPE bcdType, final int timecode)
    {
        initialize();
        
        boolean dropframe = this.dropframe = (timecode & 0x40000000) == 0x40000000;
        int hour;
        int minute;
        int second;
        int frame;
        int framesToDrop = this.framesToDrop; // per-op remove get field opcode
        int fps = this.fps; // per-op remove get field opcode
        
        switch (bcdType)
        {
            case BIG_ENDEAN:
            {
                hour = ((timecode & 0x30000000) >>> 28) * 10;
                hour += (timecode & 0x0F000000) >>> 24;
                minute = ((timecode & 0x00700000) >>> 20) * 10;
                minute += (timecode & 0x000F0000) >>> 16;
                second = ((timecode & 0x00007000) >>> 12) * 10;
                second += (timecode & 0x00000F00) >>> 8;
                frame = ((timecode & 0x00000070) >>> 4) * 10;
                frame += timecode & 0x0000000F;
                break;
            }
            case LITTLE_ENDEAN:
            {
                frame = ((timecode & 0x30000000) >>> 28) * 10;
                frame += (timecode & 0x0F000000) >>> 24;
                second = ((timecode & 0x00700000) >>> 20) * 10;
                second += (timecode & 0x000F0000) >>> 16;
                minute = ((timecode & 0x00007000) >>> 12) * 10;
                minute += (timecode & 0x00000F00) >>> 8;
                hour = ((timecode & 0x00000070) >>> 4) * 10;
                hour += timecode & 0x0000000F;
                break;
            }
            default :
                throw new IllegalArgumentException("BCD type must be one of the supported types.");
        }
        
        // Handle invalid timecode values (dropped frames are specified.  Ex. 00:01:00;00
        if (dropframe && second == 0 && minute % 10 != 0 && frame < framesToDrop)
        {
            frame = framesToDrop;
        }
        
        long nondrop_frame = hour * 3600 * fps + minute * 60 * fps + second * fps + frame;
        if (dropframe)
        {
            // Drop the extra frames.
            set(FIELD.TOTAL_FRAMES, Timecode.dropFrames(fps, nondrop_frame));
        } else
        {
            set(FIELD.TOTAL_FRAMES, nondrop_frame);
        }
    }
    
    @XmlValue()
    protected synchronized void setXmlValue(String timecode)
    throws ParseException
    {
        clear();
        if (timecode == null)
        {
            throw new ParseException("Timecode cannot be null", 0);
        }
        
        timecodeString = timecode = timecode.trim();
        
        if (!timecode.matches("[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}[:\\.;,][0-9]{1,2}[(@[0-9]{1,2})]"))
        {
            throw new ParseException("Timecode in not in valid format of \"00:00:00[:|.|;|,]00[%00]\"", 0);
        }
        
        dropframe = timecode.indexOf(';') > 0 || timecode.indexOf(',') > 0;
        //boolean dropframe = this.dropframe;
        String[] split = timecode.split("[:.;,%]");
        
        int hour = Integer.parseInt(split[0]);
        int minute = Integer.parseInt(split[1]);
        int second = Integer.parseInt(split[2]);
        int frame = Integer.parseInt(split[3]);
        
        setTime(0, hour, minute, second, frame);
        /*
        int bcd;
        bcd = ((dropframe ? 1 : 0) & 0x00000001) << 30;
        bcd |= (hour / 10 & 0x00000003) << 28;
        bcd |= (hour % 10 & 0x0000000F) << 24;
        bcd |= (minute / 10 & 0x00000007) << 20;
        bcd |= (minute % 10 & 0x0000000F) << 16;
        bcd |= (second / 10 & 0x00000007) << 12;
        bcd |= (second % 10 & 0x0000000F) << 8;
        bcd |= (frame / 10 & 0x00000007) << 4;
        bcd |= frame % 10 & 0x0000000F;
        
        setTimecode(BCD_TYPE.BIG_ENDEAN, bcd);
         */
    }
    
    public static void main(final String... args)
    {
        int fps = 30;
        int days = 2;
        String argument = null;
        List<CommandLineArgument> arguments = new ArrayList<CommandLineArgument>(args.length);
        
        for (String arg : args)
        {
            int length = arg.length();
            if (arg != null && length > 0)
            {
                if (arg.startsWith("--"))
                {
                    if (argument != null)
                    {
                        arguments.add(new CommandLineArgument(argument, null));
                    }
                    argument = arg.substring(2);
                } else if (arg.startsWith("-"))
                {
                    if (argument != null)
                    {
                        arguments.add(new CommandLineArgument(argument, null));
                    }
                    argument = arg.substring(1);
                } else if (argument != null)
                {
                    arguments.add(new CommandLineArgument(argument, arg));
                    argument = null;
                } else
                {
                    arguments.add(new CommandLineArgument(null, arg));
                    argument = null;
                }
            }
        }
        
        boolean frameCount = false;
        boolean processingStreams = false;
        List<String> streams = new LinkedList<String>();
        for (CommandLineArgument cmd : arguments)
        {
            if (cmd.key != null)
            {
                processingStreams = false;
                if ("ptp".equalsIgnoreCase(cmd.key))
                {
                    streams.add(cmd.value);
                    processingStreams = true;
                }
                
                if ("fps".equalsIgnoreCase(cmd.key))
                {
                    fps = Integer.parseInt(cmd.value);
                    frameCount = true;
                }
                
                if ("days".equalsIgnoreCase(cmd.key))
                {
                    days = Integer.parseInt(cmd.value);
                    frameCount = true;
                }
                
                if ("frameCount".equalsIgnoreCase(cmd.key))
                {
                    frameCount = true;
                }
            } else
            {
                if (processingStreams)
                {
                    streams.add(cmd.value);
                }
            }
        }
        
        if (frameCount)
        {
            Timecode.runTests(fps, days);
        }
        
        if (streams.size() > 0)
        {
            for (String stream : streams)
            {
                Timecode.performPTPTest(stream);
            }
        }
    }
    
    /**
     * Converts a Timecode to its BCD representation.
     * 
     * @param bdcType The BCD type (little or big endian)
     * @param timecode The timecode
     * @return The BCD
     */
    public static int toBCD(final BCD_TYPE bcdType, final Timecode timecode)
    {
        int bcd;
        int droppedFrames = timecode.isDropFrame() ? 1 : 0;
        int hours = (int) timecode.get(FIELD.HOUR);
        int minutes = (int) timecode.get(FIELD.MINUTE);
        int seconds = (int) timecode.get(FIELD.SECOND);
        int frames = (int) timecode.get(FIELD.FRAME);
        
        switch (bcdType)
        {
            case BIG_ENDEAN :
                bcd = (droppedFrames & 0x00000001) << 30;
                bcd |= (hours / 10 & 0x00000003) << 28;
                bcd |= (hours % 10 & 0x0000000F) << 24;
                bcd |= (minutes / 10 & 0x00000007) << 20;
                bcd |= (minutes % 10 & 0x0000000F) << 16;
                bcd |= (seconds / 10 & 0x00000007) << 12;
                bcd |= (seconds % 10 & 0x0000000F) << 8;
                bcd |= (frames / 10 & 0x00000007) << 4;
                bcd |= frames % 10 & 0x0000000F;
                return bcd;
                
            case LITTLE_ENDEAN :
                bcd = (droppedFrames & 0x00000001) << 30;
                bcd |= (frames / 10 & 0x00000003) << 28;
                bcd |= (frames % 10 & 0x0000000F) << 24;
                bcd |= (seconds / 10 & 0x00000007) << 20;
                bcd |= (seconds % 10 & 0x0000000F) << 16;
                bcd |= (minutes / 10 & 0x00000007) << 12;
                bcd |= (minutes % 10 & 0x0000000F) << 8;
                bcd |= (hours / 10 & 0x00000007) << 4;
                bcd |= hours % 10 & 0x0000000F;
                return bcd;
            default :
                throw new IllegalArgumentException("BCD type must be one of the supported types.");
        }
    }
    
    protected static int[] buildTestMatrix(final int fps, final boolean dropFrame)
    {
        int framesToDrop = (int) Math.ceil((double) fps / (double) 15); // 2 per 30 frames
        int[] matrix = new int[fps * 86400 /* Seconds in a Day */ - (dropFrame ? 1296 /* Minutes in a Day not divisible by 10 */ * framesToDrop : 0)];
        int hour = 0;
        int minute = 0;
        int second = 0;
        int frame = 0;
        
        for (int i = 0; i < matrix.length; i++)
        {
            int bcd = 0;
            if (dropFrame)
            {
                bcd = 0x00000001 << 30;
            }
            bcd += (hour / 10 & 0x00000003) << 28;
            bcd += (hour % 10 & 0x0000000F) << 24;
            bcd += (minute / 10 & 0x00000007) << 20;
            bcd += (minute % 10 & 0x0000000F) << 16;
            bcd += (second / 10 & 0x00000007) << 12;
            bcd += (second % 10 & 0x0000000F) << 8;
            bcd += (frame / 10 & 0x00000007) << 4;
            bcd += frame % 10 & 0x0000000F;
            
            matrix[i] = bcd;
            
            frame++;
            if (frame >= fps)
            {
                frame = 0;
                second++;
                if (second >= 60)
                {
                    second = 0;
                    minute++;
                    if (minute >= 60)
                    {
                        minute = 0;
                        hour++;
                        if (hour >= 24)
                        {
                            hour = 0;
                        }
                    }
                }
                
                if (dropFrame && second == 0 && minute % 10 != 0)
                {
                    frame = framesToDrop;
                }
            }
        }
        
        return matrix;
    }
    
    protected static int[] buildTestMatrix(final int fps, final boolean dropframe, final int days)
    {
        int[] matrix = new int[0];
        for (int i = 0; i < days; i++)
        {
            int[] dayMatrix = Timecode.buildTestMatrix(fps, dropframe);
            int[] newMatrix = new int[matrix.length + dayMatrix.length];
            System.arraycopy(matrix, 0, newMatrix, 0, matrix.length);
            System.arraycopy(dayMatrix, 0, newMatrix, matrix.length, dayMatrix.length);
            matrix =  newMatrix;
        }
        return matrix;
    }
    
    /**
     * Drops frames to convert a non-drop frame duration to a drop frame duration
     * 
     * @param numframes
     * @return
     */
    protected final static long dropFrames(final int framesPerSecond, final long numframes)
    {
        int framesToDrop = framesPerSecond / 15; // 2 per 30 frames
        int framesPerMinute = framesPerSecond * 60;
        int framesPerTenMinutes = framesPerMinute * 10;
        
        return numframes / framesPerMinute * framesToDrop - numframes / framesPerTenMinutes * framesToDrop;
    }
    
    protected static void runTest(final long rate, final long scale, final boolean dropframe, final int[] matrix)
    {
        for (int i = 0; i < matrix.length ; i++)
        {
            int bcd = matrix[i];
            Timecode tc = new Timecode(rate, scale, dropframe, i);
            if (Timecode.toBCD(BCD_TYPE.BIG_ENDEAN, tc) != bcd)
            {
                throw new RuntimeException("Timecode BCD mismatch for frame " + i + "! " + tc + " (bcd=" + Integer.toHexString(Timecode.toBCD(BCD_TYPE.BIG_ENDEAN, tc)) + ") does not match " + Integer.toHexString(bcd));
            }
            
            Timecode test1TC = new Timecode(rate, scale, tc.isDropFrame(), tc.get(FIELD.TOTAL_SECONDS), tc.get(FIELD.NANOSECOND));
            if(!tc.equals(test1TC))
            {
                throw new RuntimeException("Timecode Time mismatch for frame " + i + "! " + tc + " (time: seconds=" + tc.get(FIELD.TOTAL_SECONDS) + ", nanos=" + tc.get(FIELD.NANOSECOND) + ") does not match " + test1TC + " (time: seconds=" + test1TC.get(FIELD.TOTAL_SECONDS) + ", nanos=" + test1TC.get(FIELD.NANOSECOND) + ")");
            }
            /*
            Timecode test2TC = factory.newInstance(rate, scale, BCD_TYPE.BIG_ENDEAN, bcd);
            test2TC.add(FIELD.TOTAL_DAYS, tc.get(FIELD.TOTAL_DAYS));
            if(!tc.equals(test2TC))
            {
                throw new RuntimeException("Timecode mismatch for frame " + i + "! " + tc + " (bcd=" + Integer.toHexString(Timecode.toBCD(BCD_TYPE.BIG_ENDEAN, tc)) + ") does not match " + Integer.toHexString(bcd));
            }
             */
        }
    }
    
    protected static void runTests(final int fps, final int days)
    {
        int[] nondropMatrix = Timecode.buildTestMatrix(fps, false, days);
        int[] dropMatrix = Timecode.buildTestMatrix(fps, true, days);
        
        System.out.println(nondropMatrix.length + " non-dropframe timecodes in " + 24 * days + " hours.");
        System.out.println(dropMatrix.length + " dropframe timecodes in " + 24 * days + " hours.");
        
        // Get JVM to Pre-compile code
        Timecode.runTest(fps * 1000, 1000, false, nondropMatrix);
        
        // Begin test
        long start, startBlock;
        start = startBlock = System.nanoTime();
        
        Timecode.runTest(fps * 1000, 1000, false, nondropMatrix);
        System.out.println("Took " + (double) (System.nanoTime() - startBlock) / (double) 1000000 + " milliseconds to process " + nondropMatrix.length + " non-dropframe timecodes.");
        
        startBlock = System.nanoTime();
        
        Timecode.runTest(fps * 1000, 1001, true, dropMatrix);
        System.out.println("Took " + (double) (System.nanoTime() - startBlock) / (double) 1000000 + " milliseconds to process " + dropMatrix.length + " dropframe timecodes.");
        
        System.out.println("Took " + (double) (System.nanoTime() - start) / (double) 1000000 + " milliseconds to process " + (nondropMatrix.length + dropMatrix.length) + " timecodes.");
    }
    
    
    private static void performPTPTest(final String arg)
    {
        InputStream in = null;
        File file = new File(arg);
        if (file.exists() && file.canRead())
        {
            try
            {
                in = new FileInputStream(file);
            } catch (FileNotFoundException e)
            {
                // ignore
            }
        }
        
        if (in == null)
        {
            URL url = null;
            
            try
            {
                url = new URL(arg);
            } catch (MalformedURLException e)
            {
                url = Timecode.class.getResource(arg);
            }
            
            if (url != null)
            {
                try
                {
                    in = url.openStream();
                } catch (IOException e)
                {
                    System.err.println("File \"" + arg + "\" cannot be read.");
                }
            }
        }
        
        if (in != null)
        {
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            
            try
            {
                TEST_VERSION version = null;
                String line = reader.readLine();
                if (line.length() >= Timecode.SMPTE_ST2059_1_TEST.length())
                {
                    String[] parts = line.split(";");
                    line = parts.length > 0 ? parts[0].trim() : "";
                    if (Timecode.SMPTE_ST2059_1_TEST.equals(line))
                    {
                        while ((line = reader.readLine()) != null)
                        {
                            parts = line.split(";");
                            line = parts.length > 0 ? parts[0].trim() : "";
                            if (line.length() > 0)
                            {
                                if (version == null)
                                {
                                    try
                                    {
                                        version = TEST_VERSION.valueOf("v" + line);
                                    } catch (NumberFormatException e)
                                    {
                                        throw new IOException("Input Source \"" + arg + "\" is not a valid SMPTE ST2059-1 Test case file.  Invalid file format version specified \"" + line + "\".");
                                    }
                                } else
                                {
                                    switch (version)
                                    {
                                        case v1:
                                            Timecode.performPTPTest_v1(line);
                                            break;
                                        default:
                                            throw new IOException("Input Source \"" + arg + "\" is not a valid SMPTE ST2059-1 Test case file.  Invalid file format version specified \"" + version + "\".");
                                    }
                                }
                            }
                        }
                    } else
                    {
                        throw new IOException("Input Source \"" + arg + "\" is not a valid SMPTE ST2059-1 Test case file.");
                    }
                }
            } catch (IOException e)
            {
                System.err.println("Error reading Input Source \"" + arg + "\" - " + e.getMessage());
                e.printStackTrace(System.err);
            }
        } else
        {
            System.err.println("Error reading Input Source \"" + arg + "\" - Input Source could not be found.");
        }
    }
    
    private static void performPTPTest_v1(final String line)
    throws IOException
    {
        String[] fields = line.split("\\W+");
        
        if (fields.length == 15)
        {
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
            format.setTimeZone(TimeZone.getTimeZone("utc"));
            
            String name = fields[Timecode.FIELD_INDEX_NAME];
            int runtimeSeconds = Integer.parseInt(fields[Timecode.FIELD_INDEX_TEST_LENGTH_SECONDS]);;
            long runtimeNanoseconds = Long.parseLong(fields[Timecode.FIELD_INDEX_TEST_LENGTH_NANOSECONDS]);
            int currentTimeSeconds = Integer.parseInt(fields[Timecode.FIELD_INDEX_CURRENT_TIME_SECONDS]);
            long currentTimeNanoseconds = Long.parseLong(fields[Timecode.FIELD_INDEX_CURRENT_TIME_NANOSECONDS]);
            int outputControl = Integer.parseInt(fields[Timecode.FIELD_INDEX_OUTPUT_CONTROL]);
            long defaultFrameRate = Long.parseLong(fields[Timecode.FIELD_INDEX_DEFAULT_SYSTEM_FRAMERATE], 16);
            int countingMode = Integer.parseInt(fields[Timecode.FIELD_INDEX_TIME_ADDRESS_COUNT_MODE]);
            long jamTime = Long.parseLong(fields[Timecode.FIELD_INDEX_TIME_ADDRESS_DAILY_JAM_TIME]);
            long lastJamTime = Long.parseLong(fields[Timecode.FIELD_INDEX_LAST_DAILY_JAM_TIME]);
            int offsetSeconds = Integer.parseInt(fields[Timecode.FIELD_INDEX_CURRENT_LOCAL_OFFSET]) * 600;
            int jamOffsetSeconds = Integer.parseInt(fields[Timecode.FIELD_INDEX_JAM_LOCAL_OFFSET]) * 600;
            int daylightSavings = Integer.parseInt(fields[Timecode.FIELD_INDEX_DAYLIGHT_SAVINGS]);
            int jumpSeconds = Integer.parseInt(fields[Timecode.FIELD_INDEX_JUMP_SECONDS]);
            long timeOfNextJump = Long.parseLong(fields[Timecode.FIELD_INDEX_TIME_OF_NEXT_JUMP_SECONDS]);
            
            long midnightOfLastJam = (lastJamTime + jamOffsetSeconds) / Timecode.SECONDS_IN_DAY * Timecode.SECONDS_IN_DAY;
            long midnightCurrentTimeSeconds = (currentTimeSeconds + offsetSeconds) / Timecode.SECONDS_IN_DAY * Timecode.SECONDS_IN_DAY;
            
            long rate = defaultFrameRate >>> 32 & 0x0FFFFFFFFL;
            long scale = defaultFrameRate & 0x0FFFFFFFFL;
            boolean dropframe = (countingMode & 0x01) == 0x01;
            
            BigInteger startTime = BigInteger.valueOf(currentTimeSeconds).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND).add(BigInteger.valueOf(currentTimeNanoseconds));
            BigInteger endTime = startTime.add(BigInteger.valueOf(runtimeSeconds).multiply(Timecode.NANOSECONDS_IN_ONE_SECOND).add(BigInteger.valueOf(runtimeNanoseconds)));
            
            Timecode lastTC;
            Timecode tc = new Timecode(rate, scale, dropframe, startTime);
            BigInteger frameTime = tc.getTimeRoundedToFrame();
            while (frameTime.compareTo(endTime) <= 0)
            {
                System.out.println("Time Test \"" + name + "\"  : bcd = " + Integer.toHexString(Timecode.toBCD(Timecode.BCD_TYPE.BIG_ENDEAN, tc)) + "; time = " + format.format(tc.getTime()) + "; total frames = " + tc.get(FIELD.TOTAL_FRAMES) + "; frames in day = " + tc.get(FIELD.FRAME_IN_DAY) + " = " + tc);
                lastTC = tc;
                frameTime = frameTime.add(BigInteger.valueOf(tc.get(FIELD.NANOSECONDS_PER_FRAME)));
                tc = new Timecode(rate, scale, dropframe, frameTime);
                while (tc.get(FIELD.TOTAL_FRAMES) == lastTC.get(FIELD.TOTAL_FRAMES))
                {
                    frameTime = frameTime.add(Timecode.ONE_MILLION); // Add 1 Millisecond
                    tc = new Timecode(rate, scale, dropframe, frameTime);
                    //System.out.println("--> Adding Nanosecond ; time = " + format.format(tc.getTime()) + "; total frames = " + tc.get(FIELD.TOTAL_FRAMES) + "; frames in day = " + tc.get(FIELD.FRAME_IN_DAY) + " = " + tc);
                }
            }
        } else
        {
            throw new IOException("Invalid test line \"" + line + "\".  Contains " + fields.length + " fields when there should be 15. " + Arrays.toString(fields));
        }
    }
}
